/**
 * Copyright (c) 2020 lizebang
 *
 * This software is released under the MIT License.
 * https://opensource.org/licenses/MIT
 */

package jiri

import (
	"encoding/json"
	"io/ioutil"
	"net/url"
	"os"
	"path/filepath"
	"time"

	"go.fuchsia.dev/jiri/project"

	"gitee.com/banggua/ggtsync/logger"
)

type ProjectState struct {
	ProjectPath    string
	LatestUsedTime time.Time
}

func projectLocalPath(path, remote string) (string, error) {
	remoteURL, err := url.Parse(remote)
	if err != nil {
		logger.Errorf("%s %v", remote, err)
		return "", err
	}
	return filepath.Join(path, remoteURL.Host, remoteURL.Path), nil
}

func projectRemoteURL(base, remote string) (string, error) {
	baseURL, err := url.Parse(base)
	if err != nil {
		logger.Errorf("%s %s %v", base, remote, err)
		return "", err
	}
	remoteURL, err := url.Parse(remote)
	if err != nil {
		logger.Errorf("%s %s %v", base, remote, err)
		return "", err
	}
	baseURL.Path = filepath.Join(baseURL.Path, remoteURL.Host, remoteURL.Path)
	return baseURL.String(), nil
}

type ProjectStates map[project.ProjectKey]ProjectState

func getProjectStates(to string, projects project.Projects) (ProjectStates, error) {
	projectStates := make(ProjectStates)
	for key, prj := range projects {
		path, err := projectLocalPath(to, prj.Remote)
		if err != nil {
			return nil, err
		}
		projectStates[key] = ProjectState{LatestUsedTime: time.Now(), ProjectPath: path}
	}
	return projectStates, nil
}

var (
	projectStatesLock = "project-states.lock"
)

func loadProjectStates(path string) (ProjectStates, error) {
	var projectStates = make(ProjectStates)

	projectStatesFile, err := os.OpenFile(filepath.Join(path, projectStatesLock), os.O_RDONLY, 0644)
	if err != nil {
		if os.IsNotExist(err) {
			return projectStates, nil
		}
		logger.Errorf("%s %v", path, err)
		return nil, err
	}

	projectStatesJSON, err := ioutil.ReadAll(projectStatesFile)
	if err != nil {
		logger.Errorf("%s %v", path, err)
		return nil, err
	}

	err = json.Unmarshal(projectStatesJSON, &projectStates)
	if err != nil {
		os.Remove(filepath.Join(path, projectStatesLock))
		logger.Errorf("%s %v", path, err)
		return nil, err
	}

	return projectStates, nil
}

func lockProjectStates(path string, projectStates ProjectStates) error {
	projectStatesFile, err := os.OpenFile(filepath.Join(path, projectStatesLock), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
	if err != nil {
		logger.Errorf("%s %v %v", path, projectStates, err)
		return err
	}
	defer projectStatesFile.Close()

	projectStatesJSON, err := json.MarshalIndent(&projectStates, "", "\t")
	if err != nil {
		logger.Errorf("%s %v %v", path, projectStates, err)
		return err
	}

	_, err = projectStatesFile.Write(projectStatesJSON)
	if err != nil {
		logger.Errorf("%s %v %v", path, projectStates, err)
		return err
	}

	return nil
}

func computeProjects(to string, localProjectStates ProjectStates, remoteProjects project.Projects) (map[string]operation, error) {
	var operations = make(map[string]operation)

	for remoteProjectKay, remoteProject := range remoteProjects {
		if localProjectState, isExist := localProjectStates[remoteProjectKay]; isExist {
			if _, err := os.Stat(localProjectState.ProjectPath); !os.IsNotExist(err) {
				localProjectState.LatestUsedTime = time.Now()
				localProjectStates[remoteProjectKay] = localProjectState
				operations[localProjectState.ProjectPath] = &updateOperation{path: localProjectState.ProjectPath, remote: remoteProject.Remote}
			}
		} else {
			path, err := projectLocalPath(to, remoteProject.Remote)
			if err != nil {
				return nil, err
			}
			localProjectStates[remoteProjectKay] = ProjectState{LatestUsedTime: time.Now(), ProjectPath: path}
			operations[path] = &createOperation{optype: projectOp, path: path, remote: remoteProject.Remote}
		}
	}

	for localProjectStateKey, localProjectState := range localProjectStates {
		if _, isExist := operations[localProjectState.ProjectPath]; isExist {
			if time.Now().Sub(localProjectState.LatestUsedTime).Hours() > 7*24 {
				delete(localProjectStates, localProjectStateKey)
				operations[localProjectState.ProjectPath] = &deleteOperation{path: localProjectState.ProjectPath}
			}
		}
	}

	return operations, nil
}
